package com.abel.bigwater.dataserver.model.kming

import com.abel.bigwater.dataserver.model.*
import io.netty.buffer.ByteBuf
import org.slf4j.LoggerFactory

class KmingClientPacket() : TermData(KmingDataType.TermDummy) {

    var termCode = TermCodeType()

    var deviceStatus = DeviceStatus()

    var limitStatus = LimitStatus()

    var sessionStatus = SessionStatus()

    /**
     * total bytes in data, may be splited into multiple packages.
     */
    var dataLen: Int? = null

    var data: TermData? = null

    var checksum: Short? = null

    /**
     * if send in mulitple packets if length exceeds 512B
     */
    var splitPacket: Boolean = true

    /**
     * multiple packets for single sending.
     */
    var pktList: ArrayList<ByteArray> = ArrayList()

    override fun bufLength(): Int {
        return 2 + 6 + 2 + 2 + 3 + 2 + dataLen!! + 2 + 2
    }

    override fun parseBuf(buf: ByteBuf): Boolean {
        if (buf.readableBytes() < 2 + 6 + 2 + 2 + 3 + 2) {
            return false
        }

        // length.
        buf.duplicate().apply {
            if (h1 == readUnsignedByte().toInt() && h2 == readUnsignedByte().toInt()) {
            } else {
                return false
            }

            skipBytes(6 + 2 + 2 + 3)

            dataLen = readUnsignedShort()

            if (readableBytes() < dataLen!! + 2 + 2) {
                throw PacketNotEnoughException("not enough bytes: ${buf.readableBytes()} < ${dataLen!! + 2 + 2} bytes")
            }

            skipBytes(dataLen!! + 2)
            if (t1 != readUnsignedByte().toInt() && t2 != readUnsignedByte().toInt()) {
                throw PacketHeaderTailException("bad tail: $t1 $t2")
            }
        }

        // check sum.
        buf.duplicate().apply {
            val cs2 = PacketUtil.calcCrc16(ByteArray(bufLength() - 4).also { readBytes(it) })
            checksum = readShort()
            if (cs2.toShort() != checksum) {
                throw PacketChecksumException("check sum failure: $cs2 != $checksum")
            }
        }

        // parse
        buf.readBytes(bufLength()).apply {
            skipBytes(2)

            termCode.parseBuf(readBytes(termCode.bufLength()))

            deviceStatus.parseBuf(readBytes(deviceStatus.bufLength()))

            limitStatus.parseBuf(readBytes(limitStatus.bufLength()))

            sessionStatus.parseBuf(readBytes(sessionStatus.bufLength()))

            dataLen = readUnsignedShort()

            val dtype = TermData.peekType(this)
            val dataBuf = readBytes(dataLen!!)

            // b.加密字段：数据包， 不包含会话码和响应码
            if (dataLen!! > 1 && sessionStatus.encrypted) {
                ByteArray(dataLen!! - 1).also {
                    dataBuf.skipBytes(1)

                    dataBuf.readBytes(it)

                    val plain = PacketUtil.kmingDecrypt(it)
                    dataBuf.clear().writeByte(dtype!!.code.toInt()).writeBytes(plain)
                }
            }

            // only itself.
            if (pktList.isEmpty() && !sessionStatus.hasNext) {
                data = when (dtype) {
                    KmingDataType.TermUploadReq,
                    KmingDataType.TermSendData,
                    KmingDataType.TermBurst -> KmingResponseData(dtype).also { it.parseBuf(dataBuf) }

                    KmingDataType.TermHeart -> TermHeartData().also { it.parseBuf(dataBuf) }

                    KmingDataType.TermShutdownComm -> KmingRepondShutdown().also { it.parseBuf(dataBuf) }

                    else -> throw PacketCmdException("unsupported cmd: $dtype")
                }
            } else {
                // just leave helper class to parse.
                // add to the list.
                pktList.add(ByteArray(dataBuf.readableBytes()).also { dataBuf.readBytes(it) })
            }

            checksum = readShort()

            skipBytes(2)
        }

        return true
    }

    override fun pack(buf: ByteBuf) {
        pktList.clear()

        dataLen = data!!.bufLength()

        val dataBuf = buf.alloc().buffer().also { data!!.pack(it) }
        val cnt = dataLen!! / (DATA_SECRET_MAX + 1) + (if (dataLen!!.rem(DATA_SECRET_MAX + 1) > 0) 1 else 0)

        // 分包
        if (splitPacket) {
            dataBuf.skipBytes(1)
            for (i in 1..cnt) {
                val ba = ByteArray(if (i < cnt) 1 + DATA_SECRET_MAX else 1 + dataBuf.readableBytes())
                ba[0] = data!!.type.code.toByte()
                dataBuf.readBytes(ba, 1, ba.size - 1)
                pktList.add(ba)
            }
        } else {
            pktList.add(ByteArray(dataBuf.readableBytes()).also { dataBuf.readBytes(it) })
        }

        // 加密
        if (sessionStatus.encrypted) {
            for (idx in 1..pktList.size) {
                val pkt = pktList[idx - 1]
                if (pkt.size <= 1) continue

                val rst = PacketUtil.kmingEncrpt(pkt, 1)

                // 长度不一致，需要替换
                pktList[idx - 1] = byteArrayOf(data!!.type.code.toByte()).plus(rst)
            }
        }

        for (i in 1..pktList.size) {
            val pkt = pktList[i - 1]

            buf.alloc().buffer().apply {
                writeByte(h1)
                writeByte(h2)

                termCode.pack(this)

                deviceStatus.pack(this)

                limitStatus.pack(this)

                sessionStatus.hasNext = i < pktList.size
                sessionStatus.pack(this)

                writeShort(pkt.size)
                writeBytes(pkt)

                ByteArray(readableBytes()).also { ba ->
                    duplicate().readBytes(ba)
                    val cs2 = PacketUtil.calcCrc16(ba)
                    this.writeShort(cs2)
                }

                writeByte(t1)
                writeByte(t2)

                // write to buf in batch
                buf.writeBytes(this)
            }
        }
    }

    override fun toString(): String {
        return javaClass.simpleName + ":{term:${termCode.fmt},session:${sessionStatus},data:$data}"
    }

    companion object {
        const val h1 = 0xAA
        const val h2 = 0x96

        const val t1 = 0x55
        const val t2 = 0x69

        /**
         * maximum bytes of data that single packet can hold.
         * the first byte cmd-type is not counted in. the last -(3+8) is for 32-bytes times.
         */
        const val DATA_SECRET_MAX = 512 - (2 + 6 + 2 + 2 + 3 + 2 + 2 + 2) - (3 + 8)
        const val DATA_PLAIN_MAX = 512 - (2 + 6 + 2 + 2 + 3 + 2 + 2 + 2)

        private val lgr = LoggerFactory.getLogger(KmingClientPacket::class.java)
    }
}